Skip to main content
  1. Writing/

Getting Your Halo Infinite Service Record Directly From The API

·2149 words
This blog post is part of a series on exploring the Halo game API.

Overview #

One of the more interesting data points for any Halo player is generally their own service record. That is - a high-level snapshot of their historical performance in the game. This has been one of the staples of the Halo multiplayer experience for years, and Halo Infinite is no exception to that. Anyone who plays the game is likely already familiar with what you can see inside Halo Waypoint:

A more “concise” version of the service record can also be seen in-game:

Now, if you’ve been reading this blog for some time now, you likely have an idea of the fact that I am rarely satisfied with the defaults, so I started digging through the API to programatically keep track of my service record changes. Part of this work is also embedded in what I am building with OpenSpartan:

Service record view of a Halo Infinite player, as seen through OpenSpartan.
Service record view of a Halo Infinite player, as seen through OpenSpartan.

The basic service record API #

I’ll preface by saying that to get the service record, the API consumer needs to be authenticated. You can significantly simplify this process (I wrote the original article almost two years ago) by using the Microsoft Authentication Library (MSAL) and a public client application. This will eliminate the tedious process of trying to jump through the hoops of authentication codes and secrets where they don’t need to exist in the first place.

To actually get the service record, one would send a GET HTTP request with the X-343-Authorization-Spartan header containing the Spartan V4 token to the following endpoint:

https://halostats.svc.halowaypoint.com
  /hi
  /players
  /{YOUR_GAMERTAG_HERE}
  /Matchmade
  /servicerecord
  ?seasonId=Csr/Seasons/CsrSeason5-1.json
Mole out of the ground, thinking.

Wait, wait, wait... Am I correct in understanding that I need to specify my gamertag and not the Xbox ID (also known as XUID) into the API call?

Yes, indeed - you can replicate this by looking inside the Network tab of your favorite web browser when you try to look inside your own service record on Halo Waypoint. However, you can still use a XUID here if you do know it for a player, like this:

https://halostats.svc.halowaypoint.com
  /hi
  /players
  /xuid({YOUR_XUID_HERE})
  /Matchmade
  /servicerecord
  ?seasonId=Csr/Seasons/CsrSeason5-1.json

The returned data is absolutely identical, and it’s mostly a matter of preference and scenario as to what value you’d want to use here.

While we’re looking at the request itself, let’s also take a moment to acknowledge Csr/Seasons/CsrSeason5-1.json. This is an optional query string that can be used to specify the season for which the service record data is returned. This is extremely convenient if, for example, you’d want to analyze how you’re performing in, say, Season 5: Reckoning, while excluding everything else. If you omit it, you will get the overall stats for the player that include every single season the player has been active in.

Mole looking through a magnifying glass.

But where exactly do you get the value for the seasonId query parameter? Is this something one would need to guess based on past seasons?

Not really - this job actually becomes much easier once you run the request above without the seasonId parameter, because that will in turn give you a list of seasons where the player was active, like this:

"SeasonIds": [
    "Seasons/Season6.json",
    "Seasons/Season6-2.json",
    "Seasons/Season7.json",
    "Seasons/Season-Winter-Break-22.json",
    "Seasons/Season3.json",
    "Seasons/Season4.json",
    "Csr/Seasons/CsrSeason5-1.json"
]

The naming is a bit inconsistent - I haven’t actually played in Season 6 or Season 7. Those are just the numeric values for previous seasons that are out-of-sync with the reality. This is generally not a problem since these values are never actually seen by the public and are just used to fetch seasonal metadata, but that’s what we get for looking under the hood. Additional season information (such as season paths) is captured through another data file - seasoncalendar.json, available at:

https://gamecms-hacs.svc.halowaypoint.com
  /hi
  /progression
  /file
  /calendars
  /seasons
  /seasoncalendar.json

A GET request here would return data like this, giving you a hint of available season IDs:

{
    "Seasons": [
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason1-2.json",
            "OperationTrackPath": "RewardTracks/Operations/battlepass-noblesacrifice.json",
            "SeasonMetadata": "Seasons/Season1.json",
            "StartDate": {
                "ISO8601Date": "2021-06-20T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2022-05-03T17:00:00.000Z"
            }
        },
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason2-3.json",
            "OperationTrackPath": "RewardTracks/Operations/battlepass-lonewolves.json",
            "SeasonMetadata": "Seasons/Season2.json",
            "StartDate": {
                "ISO8601Date": "2022-05-03T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2022-11-08T17:00:00.000Z"
            }
        },
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason2-3.json",
            "OperationTrackPath": "RewardTracks/Operations/battlepass-WinterBreak.json",
            "SeasonMetadata": "Seasons/Season-Winter-Break-22.json",
            "StartDate": {
                "ISO8601Date": "2022-11-08T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2023-03-07T17:00:00.000Z"
            }
        },
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason3-1.json",
            "OperationTrackPath": "RewardTracks/Operations/S03BattlePass.json",
            "SeasonMetadata": "Seasons/Season3.json",
            "StartDate": {
                "ISO8601Date": "2023-03-07T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2023-06-20T17:00:00.000Z"
            }
        },
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason4-1.json",
            "OperationTrackPath": "RewardTracks/Operations/S04BattlePass.json",
            "SeasonMetadata": "Seasons/Season4.json",
            "StartDate": {
                "ISO8601Date": "2023-06-20T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2023-10-17T17:00:00.000Z"
            }
        },
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason5-1.json",
            "OperationTrackPath": "RewardTracks/Operations/S05OpPassL01.json",
            "SeasonMetadata": "Seasons/Season5-Op1.json",
            "StartDate": {
                "ISO8601Date": "2023-10-17T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2023-11-14T17:00:00.000Z"
            }
        },
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason5-1.json",
            "OperationTrackPath": "RewardTracks/Operations/S05OpPassM01.json",
            "SeasonMetadata": "Seasons/Season5-Op2.json",
            "StartDate": {
                "ISO8601Date": "2023-11-14T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2023-12-19T17:00:00.000Z"
            }
        },
        {
            "CsrSeasonFilePath": "Csr/Seasons/CsrSeason5-1.json",
            "OperationTrackPath": "RewardTracks/Operations/S05OpPassM02.json",
            "SeasonMetadata": "Seasons/Season5-Op3.json",
            "StartDate": {
                "ISO8601Date": "2023-12-19T17:00:00.000Z"
            },
            "EndDate": {
                "ISO8601Date": "2024-01-30T17:00:00.000Z"
            }
        }
    ],
    "Events": [...],
    "CareerRank": {
        "RewardTrackPath": "RewardTracks/CareerRanks/careerrank1.json"
    }
}

Or, alternatively, if you do not want to get past events (which are now transitioned to Operations), you can request:

https://gamecms-hacs.svc.halowaypoint.com
  /hi
  /Progression
  /file
  /Csr
  /Calendars
  /CsrSeasonCalendar.json

This will give you an idea of how long “ranked” seasons last, which, so far, map 1:1 to the actual season length. Notice that from Season 5 onward, CsrSeasonFilePath actually is consistent and is the same for the seasonId parameter in the service record request. Prior to that, the Csr/Season/CsrSeasonN-N.json files were separate, and using them when querying your service record would yield no useful data at all.

This also reminds me that we should talk a bit about the structure of the response. The layout of the data resembles this format:

graph LR; SR[ServiceRecord]; SQ[Subqueries]; SI[SeasonIds]; GVC[GameVariantCategories]; IR[IsRanked]; PAI[PlaylistAssetIds]; MC[MatchesCompleted]; W[Wins]; L[Losses]; T[Ties]; CS[CoreStats]; TP[TimePlayed]; SR --> SQ; SQ --> SI; SQ --> GVC; SQ --> IR; SQ --> PAI; SR --> TP; SR --> MC; SR --> W; SR --> L; SR --> T; SR --> CS; CS --> Score; CS --> PersonalScore; CS --> RoundsWon; CS --> RoundsLost; CS --> RoundsTied; CS --> Kills; CS --> Deaths; CS --> Assists; CS --> AverageKDA; CS --> Suicides; CS --> Betrayals; CS --> GrenadeKills; CS --> HeadshotKills; CS --> MeleeKills; CS --> PowerWeaponKills; CS --> ShotsFired; CS --> ShotsHit; CS --> Accuracy; CS --> DamageDealt; CS --> DamageTaken; CS --> CalloutAssists; CS --> VehicleDestroys; CS --> DriverAssists; CS --> Hijacks; CS --> EmpAssists; CS --> MaxKillingSpree; CS --> MDLS[Medals]; MDLS --> MEDA[MedalA]; MEDA --> NIA[NameId]; MEDA --> CMA[Count]; MEDA --> TPSAA[TotalPersonalScoreAwarded]; MDLS --> MEDB[MedalB]; MEDB --> NIB[NameId]; MEDB --> CMC[Count]; MEDB --> TPSAB[TotalPersonalScoreAwarded]; CS --> PS[PersonalScores]; PS --> PSA[PersonalScoreA]; PSA --> NIPSA[NameId]; PSA --> CMPSA[Count]; PSA --> TPSAPSA[TotalPersonalScoreAwarded]; PS --> PSB[PersonalScoreB]; PSB --> NIPSB[NameId]; PSB --> CMPSB[Count]; PSB --> TPSAPSB[TotalPersonalScoreAwarded]; CS --> Spawns; CS --> ObjectivesCompleted; SR --> CTFS[CaptureTheFlagStats]; CTFS --> FlagCaptureAssists CTFS --> FlagCaptures CTFS --> FlagCarriersKilled CTFS --> FlagGrabs CTFS --> FlagReturnersKilled CTFS --> FlagReturns CTFS --> FlagSecures CTFS --> FlagSteals CTFS --> KillsAsFlagCarrier CTFS --> KillsAsFlagReturner CTFS --> TimeAsFlagCarrier SR --> ES[EliminationStats] ES --> AlliesRevived ES --> EliminationAssists ES --> Eliminations ES --> EnemyRevivesDenied ES --> Executions ES --> KillsAsLastPlayerStanding ES --> LastPlayersStandingKilled ES --> RoundsSurvived ES --> TimesRevivedByAlly SR --> EXS[ExtractionStats] EXS --> SuccessfulExtractions EXS --> ExtractionConversionsDenied EXS --> ExtractionConversionsCompleted EXS --> ExtractionInitiationsDenied EXS --> ExtractionInitiationsCompleted SR --> INFS[InfectionStats] INFS --> AlphasKilled INFS --> SpartansInfected INFS --> SpartansInfectedAsAlpha INFS --> KillsAsLastSpartanStanding INFS --> LastSpartansStandingInfected INFS --> RoundsAsAlpha INFS --> RoundsAsLastSpartanStanding INFS --> RoundsFinishedAsInfected INFS --> RoundsSurvivedAsSpartan INFS --> RoundsSurvivedAsLastSpartanStanding INFS --> TimeAsLastSpartanStanding INFS --> InfectedKilled SR --> ODDS[OddballStats] ODDS --> KillsAsSkullCarrier ODDS --> LongestTimeAsSkullCarrier ODDS --> SkullCarriersKilled ODDS --> SkullGrabs ODDS --> TimeAsSkullCarrier ODDS --> SkullScoringTicks SR --> ZS[ZonesStats] ZS --> ZoneCaptures ZS --> ZoneDefensiveKills ZS --> ZoneOffensiveKills ZS --> ZoneSecures ZS --> TotalZoneOccupationTime ZS --> ZoneScoringTicks SR --> SSS[StockpileStats] SSS --> KillsAsPowerSeedCarrier SSS --> PowerSeedCarriersKilled SSS --> PowerSeedsDeposited SSS --> PowerSeedsStolen SSS --> TimeAsPowerSeedCarrier SSS --> TimeAsPowerSeedDriver

Wow that was a lot. If you prefer to look at a JSON version, here is my own recent service record snapshot. The service record here returns quite a bit of information related to in-game performance (across all supported game modes, like Capture The Flag, Extraction, Infection, and more), medals, as well as breakdown of personal scores. The cryptic GameVariantCategories represents the game variants in which the player participated. In the JSON representation, we get numbers, but if we request the XML version of the exact same service record (with Accept: application/xml header attached), we quickly learn that each value has a name, like this:

<d3p1:GameVariantCategory>MultiplayerFiesta</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerBastion</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerStrongholds</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerSlayer</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerExtraction</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerOddball</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerCtf</d3p1:GameVariantCategory>

Looking into every single value is going to be saved for another post. PlaylistAssetIds, in turn, represents each playlist in which the player participated during the season. IsRanked hints at whether the player has acquired a rank during a given season.

From that entire list, this likely is going to be the most confusing one - what the heck does CoreStats > PersonalScores represent? The data structures for each personal score is basically the same as for medals, as we’ve seen through this static file:

https://gamecms-hacs.svc.halowaypoint.com
  /hi
  /Waypoint
  /file
  /medals
  /metadata.json

There is no such metadata file that we see from existing requests for personal scores. However, we can kind of deduce what the points are awarded for if we look at some of the match details and try to correlate them with the NameId values. For example, here is one of my recent Husky Raid (CTF) match performances:

Personal match results, as seen through Halo Waypoint.
Personal match results, as seen through Halo Waypoint.

The personal scores for this same match, also for me, are as follows:

"PersonalScores": [
    {
        "NameId": 1024030246,
        "Count": 15,
        "TotalPersonalScoreAwarded": 1500
    },
    {
        "NameId": 152718958,
        "Count": 9,
        "TotalPersonalScoreAwarded": 90
    },
    {
        "NameId": 638246808,
        "Count": 10,
        "TotalPersonalScoreAwarded": 500
    },
    {
        "NameId": 1267013266,
        "Count": 2,
        "TotalPersonalScoreAwarded": 20
    },
    {
        "NameId": 601966503,
        "Count": 1,
        "TotalPersonalScoreAwarded": 300
    },
    {
        "NameId": 249491819,
        "Count": 1,
        "TotalPersonalScoreAwarded": -100
    }
],

From here, we can relatively easily draw the following parallels:

Name ID Points Per Award Times Awarded Name
1024030246 100 15 Kills
152718958 10 9 Callout Assists
638246808 50 10 Assists
1267013266 10 2 Flag Secures
601966503 300 1 Flag Captures
249491819 -100 -100 Self-Destructions

Notice that the Flag Secures personal score is not really extrapolated from the visible stats. Instead, it’s inferred from the CaptureTheFlagStats property (since Husky Raid is, after all, chaotic Capture The Flag):

"CaptureTheFlagStats": {
    "FlagCaptureAssists": 0,
    "FlagCaptures": 1,
    "FlagCarriersKilled": 0,
    "FlagGrabs": 6,
    "FlagReturnersKilled": 0,
    "FlagReturns": 0,
    "FlagSecures": 2,
    "FlagSteals": 0,
    "KillsAsFlagCarrier": 0,
    "KillsAsFlagReturner": 0,
    "TimeAsFlagCarrier": "PT6.9S"
}

Similar approaches can be taken with other data points, and because personal score NameId values are not really captured in a single accessible file, we’ll have to do some manual work to understand them, if we ever were in a position where we need to make those a bit more human-readable.

Player rank #

If you look through the content above, you will notice that the service record barely talks about the player rank. We know that someone is ranked but what is their specific rank? That data is captured through the skill endpoint:

https://skill.svc.halowaypoint.com
  /hi
  /playlist
  /{RANKED_PLAYLIST_ID}
  /csrs?players={YOUR_XUID}

For example, for the Ranked Doubles playlist (with the ID equal to fa5aa2a3-2428-4912-a023-e1eeea7b877c), the response for my player will be:

{
    "Value": [
        {
            "Id": "MY_XUID",
            "ResultCode": 0,
            "Result": {
                "Current": {
                    "Value": 997,
                    "MeasurementMatchesRemaining": 0,
                    "Tier": "Platinum",
                    "TierStart": 950,
                    "SubTier": 1,
                    "NextTier": "Platinum",
                    "NextTierStart": 1000,
                    "NextSubTier": 2,
                    "InitialMeasurementMatches": 5,
                    "DemotionProtectionMatchesRemaining": 0,
                    "InitialDemotionProtectionMatches": 3
                },
                "SeasonMax": {
                    "Value": 1005,
                    "MeasurementMatchesRemaining": 0,
                    "Tier": "Platinum",
                    "TierStart": 1000,
                    "SubTier": 2,
                    "NextTier": "Platinum",
                    "NextTierStart": 1050,
                    "NextSubTier": 3,
                    "InitialMeasurementMatches": 5,
                    "DemotionProtectionMatchesRemaining": 0,
                    "InitialDemotionProtectionMatches": 3
                },
                "AllTimeMax": {
                    "Value": 1005,
                    "MeasurementMatchesRemaining": 0,
                    "Tier": "Platinum",
                    "TierStart": 1000,
                    "SubTier": 2,
                    "NextTier": "Platinum",
                    "NextTierStart": 1050,
                    "NextSubTier": 3,
                    "InitialMeasurementMatches": 5,
                    "DemotionProtectionMatchesRemaining": 0,
                    "InitialDemotionProtectionMatches": 3
                }
            }
        }
    ]
}

CSR stands for Competitive Skill Rank. With the data above, you can see data not only for yourself, but also for other players, the XUIDs for which can be chained in the request under players. You will be able to see their current rank, maximum rank for the season, as well as the all-time maximum across their entire career.

As an added bonus, you can constrain the rank lookup to a given season with the season query argument:

https://skill.svc.halowaypoint.com
  /hi
  /playlist
  /{RANKED_PLAYLIST_ID}
  /csrs?players={YOUR_XUID}&season={SEASON_ID}

The {SEASON_ID} is a simplified season identifier, as seen from the values above, like CsrSeason5-1 (without the path prefixes).

As an aside, remember how I talked about playlist weights in a previous post? You can use that information to get more details about the ranked playlist through the discovery endpoint, like this (for Ranked Doubles mentioned above):

https://discovery-infiniteugc.svc.halowaypoint.com
  /hi
  /Playlists
  /fa5aa2a3-2428-4912-a023-e1eeea7b877c
  /versions
  /3da879ff-1df5-40c9-973f-97202d522288

Or, for an abridged version:

https://gamecms-hacs.svc.halowaypoint.com
  /hi
  /Multiplayer
  /file
  /playlists
  /assets
  /fa5aa2a3-2428-4912-a023-e1eeea7b877c.json

Conclusion #

With the two API endpoints above (service record and CSR) you can get regular snapshots of your own in-game performance. That’s what I use to populate my very own Halo stats page on this blog.